package org.aceor.mddal.group;

import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executor;

/**
 * Created by lxue on 16/3/27.
 */
public class GroupConnection implements Connection {

    private static final Logger log = LoggerFactory.getLogger(GroupConnection.class);

    private GroupDataSource groupDatasource;
    private Connection rBaseConnection;
    private Connection wBaseConnection;
    private DataSourceWrapper rBaseDsWrapper;
    private DataSourceWrapper wBaseDsWrapper;

    private String username;
    private String password;

    private boolean isAutoCommit = true;

    private boolean closed;

    //jdbc规范: connection关闭要保证所有statement关闭,因此这里持有所有的statement引用
    private Set<GroupStatement> openedStatements = Sets.newHashSet();

    public GroupConnection(GroupDataSource groupDatasource) {
        this.groupDatasource = groupDatasource;
    }

    public GroupConnection(String username, String password, GroupDataSource groupDatasource) {
        this.username = username;
        this.password = password;
        this.groupDatasource = groupDatasource;
    }

    @Override
    public Statement createStatement() throws SQLException {
        GroupStatement groupStatement = new GroupStatement(groupDatasource, this);
        openedStatements.add(groupStatement);
        return groupStatement;
    }

    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        GroupPreparedStatement groupPreparedStatement = new GroupPreparedStatement(groupDatasource, this, sql);
        openedStatements.add(groupPreparedStatement);
        return groupPreparedStatement;
    }

    @Override
    public CallableStatement prepareCall(String sql) throws SQLException {
        return null;
    }

    @Override
    public String nativeSQL(String sql) throws SQLException {
        return null;
    }

    @Override
    public void setAutoCommit(boolean autoCommit) throws SQLException {
        this.isAutoCommit = autoCommit;
        //忽略读
        if (null != wBaseConnection) {
            wBaseConnection.setAutoCommit(autoCommit);
        }
    }

    @Override
    public boolean getAutoCommit() throws SQLException {
        return isAutoCommit;
    }

    @Override
    public void commit() throws SQLException {
        if (isAutoCommit) {
            return;
        }
        //忽略读
        if (null != wBaseConnection) {
            try {
                wBaseConnection.commit();
            } catch (SQLException e) {
                log.error("Commit failed on " + this.wBaseDsWrapper.getDataSourceKey() + ":" + e.getMessage());
                throw e;
            }
        }

    }

    @Override
    public void rollback() throws SQLException {
        if (isAutoCommit) {
            return;
        }

        if (wBaseConnection != null) {
            try {
                wBaseConnection.rollback();
            } catch (SQLException e) {
                log.error("Rollback failed on " + this.wBaseDsWrapper.getDataSourceKey() + ":" + e.getMessage());
                throw e;
            }
        }
    }

    @Override
    public void close() throws SQLException {
        if (closed) {
            return;
        }
        closed = true;
        try {
            for (GroupStatement statement : openedStatements) {
                statement.close(false);
            }
            if (rBaseConnection != null && !rBaseConnection.isClosed()) {
                rBaseConnection.close();
            }
            if (wBaseConnection != null && !wBaseConnection.isClosed()) {
                wBaseConnection.close();
            }
        } finally {
            rBaseConnection = null;
            wBaseConnection = null;
        }

    }

    @Override
    public boolean isClosed() throws SQLException {
        return closed;
    }

    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        return null;
    }

    @Override
    public void setReadOnly(boolean readOnly) throws SQLException {

    }

    @Override
    public boolean isReadOnly() throws SQLException {
        return false;
    }

    @Override
    public void setCatalog(String catalog) throws SQLException {

    }

    @Override
    public String getCatalog() throws SQLException {
        return null;
    }

    @Override
    public void setTransactionIsolation(int level) throws SQLException {

    }

    @Override
    public int getTransactionIsolation() throws SQLException {
        return 0;
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        return null;
    }

    @Override
    public void clearWarnings() throws SQLException {

    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
        GroupStatement statement = (GroupStatement) createStatement();
        statement.setResultSetConcurrency(resultSetConcurrency);
        statement.setResultSetType(resultSetType);
        return statement;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        GroupPreparedStatement statement = (GroupPreparedStatement) prepareStatement(sql);
        statement.setResultSetConcurrency(resultSetConcurrency);
        statement.setResultSetType(resultSetType);
        return statement;
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        return null;
    }

    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        return null;
    }

    @Override
    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {

    }

    @Override
    public void setHoldability(int holdability) throws SQLException {

    }

    @Override
    public int getHoldability() throws SQLException {
        return 0;
    }

    @Override
    public Savepoint setSavepoint() throws SQLException {
        return null;
    }

    @Override
    public Savepoint setSavepoint(String name) throws SQLException {
        return null;
    }

    @Override
    public void rollback(Savepoint savepoint) throws SQLException {

    }

    @Override
    public void releaseSavepoint(Savepoint savepoint) throws SQLException {

    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        GroupStatement statement = (GroupStatement) createStatement();
        statement.setResultSetConcurrency(resultSetConcurrency);
        statement.setResultSetHoldability(resultSetHoldability);
        statement.setResultSetType(resultSetType);
        return statement;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        GroupPreparedStatement statement = (GroupPreparedStatement) prepareStatement(sql);
        statement.setResultSetConcurrency(resultSetConcurrency);
        statement.setResultSetHoldability(resultSetHoldability);
        statement.setResultSetType(resultSetType);
        return statement;
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
        GroupPreparedStatement statement = (GroupPreparedStatement) prepareStatement(sql);
        statement.setAutoGeneratedKeys(autoGeneratedKeys);
        return statement;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
        GroupPreparedStatement statement = (GroupPreparedStatement) prepareStatement(sql);
        statement.setColumnIndexes(columnIndexes);
        return statement;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
        GroupPreparedStatement statement = (GroupPreparedStatement) prepareStatement(sql);
        statement.setColumnNames(columnNames);
        return statement;
    }

    @Override
    public Clob createClob() throws SQLException {
        return null;
    }

    @Override
    public Blob createBlob() throws SQLException {
        return null;
    }

    @Override
    public NClob createNClob() throws SQLException {
        return null;
    }

    @Override
    public SQLXML createSQLXML() throws SQLException {
        return null;
    }

    @Override
    public boolean isValid(int timeout) throws SQLException {
        return false;
    }

    @Override
    public void setClientInfo(String name, String value) throws SQLClientInfoException {

    }

    @Override
    public void setClientInfo(Properties properties) throws SQLClientInfoException {

    }

    @Override
    public String getClientInfo(String name) throws SQLException {
        return null;
    }

    @Override
    public Properties getClientInfo() throws SQLException {
        return null;
    }

    @Override
    public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
        return null;
    }

    @Override
    public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
        return null;
    }

    @Override
    public void setSchema(String schema) throws SQLException {

    }

    @Override
    public String getSchema() throws SQLException {
        return null;
    }

    @Override
    public void abort(Executor executor) throws SQLException {

    }

    @Override
    public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {

    }

    @Override
    public int getNetworkTimeout() throws SQLException {
        return 0;
    }

    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return this.getClass().isAssignableFrom(iface);
    }

    @SuppressWarnings("unchecked")
    public <T> T unwrap(Class<T> iface) throws SQLException {
        try {
            return (T) this;
        } catch (Exception e) {
            throw new SQLException(e);
        }
    }

    void removeOpenedStatements(Statement statement) {
        if (!openedStatements.remove(statement)) {
            log.warn("current statmenet ：" + statement + " doesn't exist!");
        }
    }

    /**
     * 第一次创建时使用,否则会通过连接复用获取
     */
    Connection createNewConnection(DataSourceWrapper dsw, boolean isRead) throws SQLException {
        //这个方法只发生在第一次建立读/写连接的时候，以后都是复用了
        Connection conn;
        if (username != null) {
            conn = dsw.getConnection(username, password);
        } else {
            conn = dsw.getConnection();
        }

        //为了保证事务正确关闭，在事务状态下只设置写连接
        setBaseConnection(conn, dsw, isRead && isAutoCommit);

        //只在写连接上调用setAutoCommit
        if (!isRead || !isAutoCommit)
            conn.setAutoCommit(isAutoCommit); //新建连接的AutoCommit要与当前isAutoCommit的状态同步

        return conn;
    }

    private void setBaseConnection(Connection baseConnection, DataSourceWrapper dsw, boolean isRead) throws SQLException {
        if (baseConnection == null) {
            log.warn("setBaseConnection to null !!");
        }

        if (isRead) {
            closeReadConnection();
        } else {
            closeWriteConnection();
        }
        if (isRead) {
            //非事务状态下读
            rBaseConnection = baseConnection;
            this.rBaseDsWrapper = dsw;
        } else {
            //非事务状态写,设置写连接;事务状态下,不论写或读都设置写连接.因此,写连接对应的ds必须提供读
            wBaseConnection = baseConnection;
            this.wBaseDsWrapper = dsw;
        }
    }

    private void closeReadConnection() {
        //r|wBaseConnection可能指向同一个对象，如果另一个引用在用，就不去关闭
        if (rBaseConnection != null && rBaseConnection != wBaseConnection) {
            try {
                rBaseConnection.close(); // 旧的baseConnection要关闭
            } catch (SQLException e) {
                log.error("close rBaseConnection failed.", e);
            }
            rBaseDsWrapper = null;
            rBaseConnection = null;
        }
    }

    private void closeWriteConnection() throws SQLException {
        if (null == wBaseConnection) {
            return;
        }
        //r|wBaseConnection可能指向同一个对象，如果另一个引用在用，就不去关闭
        if (wBaseConnection != null && rBaseConnection != wBaseConnection) {
            try {
                wBaseConnection.close(); // 旧的baseConnection要关闭
            } catch (SQLException e) {
                log.error("close wBaseConnection failed.", e);
            }
            wBaseDsWrapper = null;
            wBaseConnection = null;
        } else {
            //保证读连接上事务关闭
            //TODO: 自己加的,不知道会不会导致问题出现
            if (rBaseConnection != null && !rBaseConnection.getAutoCommit()) {
                rBaseConnection.setAutoCommit(true);
                log.info("关闭读复用而来的事务性写连接 ==> 由于读写连接引用相同,写连接关闭跳过,读连接设置非事务状态");
            }
        }
        //TODO: LEVEL1 ==> 如果写是通过读连接服用的,且获取写的时候开启了事务,那么这时的关闭,需要恢复事务状态吗
        //情况描述: 先创建了一个读 ==> 获取写时,读连接复用 ==> 复用得到的写连接设置事务,因此读也被设置事务
        //                      ==> 新创建写 ==>关闭原先的写,由于读写复用,写跳过关闭
        //结果,读保留了写的事务
    }



    public Connection getBaseConnection(String sql, boolean isRead) throws SQLException {
        //先写后写,先写后读,先读后写,先读后读
        //为了保证事务正确关闭，在事务状态下只会取回写连接
        if (isRead && isAutoCommit) {
            //非事务状态下,找读连接
            //只要有写连接，并且对应的库可读，则复用。否则返回读连接
            return wBaseConnection != null && wBaseDsWrapper.hasReadWeight() ? wBaseConnection : rBaseConnection;
            //先写后读，重用写连接读后，rBaseConnection仍然是null
        } else {
            //事务状态下写,非事务性下写三种情况,都是获取写连接.
            if (wBaseConnection != null) {
                return wBaseConnection;
            } else if (rBaseConnection != null && rBaseDsWrapper.hasWriteWeight()) {
                //在写连接null的情况下，如果读连接已经建立，且对应的库可写，则复用
                wBaseConnection = rBaseConnection;//wBaseConnection赋值，以确保事务能够正确提交回滚
                //在写连接上设置事务
                if (!isAutoCommit) {
                    wBaseConnection.setAutoCommit(isAutoCommit);
                }
                wBaseDsWrapper = rBaseDsWrapper;
                return wBaseConnection;
            } else {
                return null;
            }
        }
    }
}
